import math
from multiprocessing.spawn import prepare
from numba import njit, int64, float64
import numpy as np


@njit(
    int64[:, :, :](int64[:, :, :]),
    parallel=False, cache=True)
def initial_product_operation_detail(product_operations_detail):
    return np.where(
        product_operations_detail == 0, int(2147483640), product_operations_detail)


@njit(
    (int64[:, :],  int64,  int64[:], int64[:, :],  int64),
    parallel=False, cache=True)
def initial_OS(OS, size, product_operations, product_route, length):
    '''
    随机排列OS
    '''
    # 工件个数
    product_total_num = product_operations.shape[0]
    # OS左索引
    left_index = 0
    # OS右索引
    right_index = 0
    for product_num in range(product_total_num):
        # 当前工件的工序数
        operations = product_operations[product_num]
        # 更新左索引
        left_index = right_index
        # 更新右索引
        right_index += operations
        # 赋值编码
        for OS_index in range(size):
            OS[OS_index][left_index:right_index] = product_num
    operation_count = np.empty(
        shape=(1, product_total_num), dtype=np.int64).flatten()
    # 随机打乱个体编码
    for OS_index in range(size):
        np.random.shuffle(OS[OS_index])
        # operation_count.fill(0)
        # # 合并23工艺
        # for position in range(length):
        #     product_num = OS[OS_index][position]
        #     operation_num = operation_count[product_num]
        #     operation_count[product_num] += 1
        #     route_num = product_route[product_num][operation_num]
        #     if route_num == 1:
        #         next_position = position+1
        #         while(OS[OS_index][next_position] != product_num):
        #             next_position += 1
        #         OS[OS_index][position+2:next_position +
        #                      1] = OS[OS_index][position+1:next_position]
        #         OS[OS_index][position+1] = product_num


@njit(
    (int64[:], int64, int64),
    parallel=False, cache=True)
def add_one_item(list, value, length):
    if length == 0:
        list[0] = value
        return
    index = np.searchsorted(list[0:length], value)
    list[index+1:length+1] = list[index: length]
    list[index] = value


@njit(
    (int64[:], int64[:], int64[:],
     int64, int64, int64,
     int64),
    parallel=False, cache=True)
def add_tree_item(end_time_list, job_list, operation_list,
                  end_time, product_num, operation_num,
                  length):
    if length == 0:
        end_time_list[0] = end_time
        job_list[0] = product_num
        operation_list[0] = operation_num
        return
    index = np.searchsorted(end_time_list[0:length], end_time)
    end_time_list[index+1:length+1] = end_time_list[index: length]
    end_time_list[index] = end_time
    job_list[index+1:length+1] = job_list[index: length]
    job_list[index] = product_num
    operation_list[index+1:length+1] = operation_list[index: length]
    operation_list[index] = operation_num


def decode(OS,
           decode_results, code_indexs,
           machine_total_num,
           product_operation_detail,
           candidate_machine, candidate_machine_index,
           begin_time, end_time,
           begin_time_lists,  end_time_lists,
           job_lists, operation_lists,
           product_operation,
           selected_machine,
           machine_operations,
           product_route, product_joints,
           result):
    '''
    解码 可修改加速但没必要
    '''

    for code_index in code_indexs:
        result.fill(int64(2000000000))
        decode_one(OS[code_index],
                   machine_total_num,
                   product_operation_detail,
                   candidate_machine, candidate_machine_index,
                   begin_time, end_time,
                   begin_time_lists,  end_time_lists,
                   job_lists, operation_lists,
                   product_operation,
                   selected_machine,
                   machine_operations,
                   product_route, product_joints,
                   result
                   )
        decode_results[code_index] = result[0]


@njit(
    (int64[:],
     int64,
     int64[:, :, :],
     int64[:, :, :], int64[:, :],
     int64[:, :, :], int64[:, :, :],
     int64[:, :], int64[:, :],
     int64[:, :], int64[:, :],
     int64[:],
     int64[:, :],
     int64[:],
     int64[:, :], int64[:],
     int64[:]),
    parallel=False, cache=True)
def decode_one(OS,
               machine_total_num,
               product_operation_detail,
               candidate_machine, candidate_machine_index,
               begin_time, end_time,
               begin_time_lists,  end_time_lists,
               job_lists, operation_lists,
               product_operation,
               selected_machine,
               machine_operations,
               product_route, product_joints,
               result):
    '''
    解码一个个体
    '''
    # 各个工序的详细信息矩阵
    # jobs_operations_detail = initial_jobs_operations_detail(
    #     jobs_operations_detail)
    # 初始化开始时间矩阵
    begin_time.fill(-1)
    # 初始化结束时间矩阵
    end_time.fill(-1)
    # 初始化该哪个工序的矩阵
    product_operation.fill(0)
    # 机器最终时间
    machine_time = np.zeros(shape=(1, machine_total_num),
                            dtype=np.int64).flatten()
    # 初始化记录机器是否开始加工过的矩阵
    # machine_operationed.fill(0)
    # 初始化各工序所选机器矩阵
    selected_machine.fill(0)
    # # 初始化各工序所用时间矩阵
    # selected_machine_time.fill(0)
    # 初始化各机器开始时间列表
    # begin_time_lists.fill(0)
    # # 初始化各机器结束时间列表
    # end_time_lists.fill(0)
    # 初始各个机器加工步骤数
    machine_operations.fill(0)
    # 依次读取的位置
    # MS_position = 0
    # 生成调度 ### begin
    for OS_position in range(len(OS)):
        # 产品编号
        product_num = OS[OS_position]
        # 工序编号
        operation_num = product_operation[product_num]
        # 更新工序编号矩阵
        product_operation[product_num] += 1
        # 加工类型
        route_type = product_route[product_num][operation_num]
        # 加工机器编号
        candidate_machine_list = candidate_machine[product_num][operation_num][:
                                                                               candidate_machine_index[product_num][operation_num]]
        # 产品节数
        product_joint = product_joints[product_num]
        # A
        if route_type == 0:
            # 机器第一次开工
            candidate_machine_time = machine_time[candidate_machine_list]
            selected_machine_num = candidate_machine_list[np.argmin(
                candidate_machine_time)]
            begin_time_o = machine_time[selected_machine_num]
            # begin_time_o = begin_time_o + 3-begin_time_o % 3
            operation_time = product_operation_detail[product_num][operation_num][selected_machine_num]
            end_time_o = begin_time_o + operation_time
            add_one_item(begin_time_lists[selected_machine_num], begin_time_o,
                         machine_operations[selected_machine_num])
            add_tree_item(end_time_lists[selected_machine_num], job_lists[selected_machine_num], operation_lists[selected_machine_num],
                          end_time_o, product_num, operation_num,
                          machine_operations[selected_machine_num])
            machine_time[selected_machine_num] = end_time_o
            machine_operations[selected_machine_num] += 1
            begin_time[selected_machine_num][product_num][operation_num] = begin_time_o
            end_time[selected_machine_num][product_num][operation_num] = end_time_o
            selected_machine[product_num][operation_num] = selected_machine_num
        if route_type == 1:
            # candidate_machine_time = machine_time[candidate_machine_list]
            # selected_machine_num = candidate_machine_list[np.argmin(
            #     candidate_machine_time)]
            # last_operation_machine_num = selected_machine[product_num][operation_num-1]
            # last_operation_end_time = end_time[last_operation_machine_num][product_num][operation_num-1]
            # begin_time_o = max(
            #     machine_time[selected_machine_num], last_operation_end_time)
            # operation_time = product_operation_detail[product_num][operation_num][selected_machine_num]
            # end_time_o = begin_time_o + operation_time
            # add_one_item(begin_time_lists[selected_machine_num], begin_time_o,
            #              machine_operations[selected_machine_num])
            # add_tree_item(end_time_lists[selected_machine_num], job_lists[selected_machine_num], operation_lists[selected_machine_num],
            #               end_time_o, product_num, operation_num,
            #               machine_operations[selected_machine_num])
            # machine_time[selected_machine_num] = end_time_o
            # machine_operations[selected_machine_num] += 1
            # begin_time[selected_machine_num][product_num][operation_num] = begin_time_o
            # end_time[selected_machine_num][product_num][operation_num] = end_time_o
            # selected_machine[product_num][operation_num] = selected_machine_num
            pass
        if route_type == 2:
            candidate_machine_time = machine_time[candidate_machine_list]
            selected_machine_num = candidate_machine_list[np.argmin(
                candidate_machine_time)]
            operation_time = product_operation_detail[product_num][
                operation_num][selected_machine_num]*product_joint
            last_operation_num = operation_num - 1
            last_candidate_machine_list = candidate_machine[product_num][last_operation_num][:
                                                                                             candidate_machine_index[product_num][last_operation_num]]
            last_candidate_machine_time = machine_time[last_candidate_machine_list].copy(
            )
            # 上道工序的最优时间
            for i in range(len(last_candidate_machine_list)):
                last_last_selected_machine = selected_machine[product_num][operation_num-2]
                last_last_operation_end_time = end_time[last_last_selected_machine][product_num][operation_num-2]
                machine_num = last_candidate_machine_list[i]
                last_operation_time = product_operation_detail[
                    product_num][last_operation_num][machine_num]
                if last_operation_time % 3 != 0:
                    last_operation_time = last_operation_time + 3-last_operation_time % 3
                prepare_time = 0
                route_B_index = np.where(product_route[product_num] == 1)[0]
                this_route_B_index = np.where(
                    route_B_index == last_operation_num)[0]
                machine_last_operation_index = machine_operations[machine_num]-1
                machine_last_product = job_lists[machine_num][machine_last_operation_index]
                machine_last_operation = operation_lists[machine_num][machine_last_operation_index]
                if this_route_B_index == 0:
                    prepare_time = 30
                else:
                    last_B_operation_num = route_B_index[this_route_B_index-1]
                    last_B_machine_num = selected_machine[product_num][last_B_operation_num]
                    if last_B_machine_num != machine_num:
                        prepare_time = 30
                if machine_last_product != product_num:
                    prepare_time = 30
                last_operation_time += prepare_time
                begin_time_last_operation = max(
                    machine_time[machine_num], last_last_operation_end_time)
                last_candidate_machine_time[i] = begin_time_last_operation + \
                    last_operation_time
            begin_time_o = machine_time[selected_machine_num]
            # begin_time_o = begin_time_o + 3-begin_time_o % 3
            min_machine_index = np.argmin(last_candidate_machine_time)
            last_operation_machine_num = last_candidate_machine_list[min_machine_index]
            last_operation_end_time = last_candidate_machine_time[min_machine_index]
            begin_time_o = max(begin_time_o, last_operation_end_time)
            end_time_o = begin_time_o + operation_time
            # 更新当前工序
            add_one_item(begin_time_lists[selected_machine_num], begin_time_o,
                         machine_operations[selected_machine_num])
            add_tree_item(end_time_lists[selected_machine_num], job_lists[selected_machine_num], operation_lists[selected_machine_num],
                          end_time_o, product_num, operation_num,
                          machine_operations[selected_machine_num])
            machine_time[selected_machine_num] = end_time_o
            machine_operations[selected_machine_num] += 1
            begin_time[selected_machine_num][product_num][operation_num] = begin_time_o
            end_time[selected_machine_num][product_num][operation_num] = end_time_o
            selected_machine[product_num][operation_num] = selected_machine_num
            # 上道工序开始时间
            last_operation_time = product_operation_detail[
                product_num][last_operation_num][last_operation_machine_num]
            if last_operation_time % 3 != 0:
                last_operation_time = last_operation_time + 3-last_operation_time % 3
            # route_B_index = np.where(product_route[product_num] == 1)[0]
            # this_route_B_index = np.where(
            #     route_B_index == last_operation_num)[0]
            # machine_last_operation_index = machine_operations[last_operation_machine_num]-1
            # machine_last_product = job_lists[last_operation_machine_num][machine_last_operation_index]
            # machine_last_operation = operation_lists[last_operation_machine_num][machine_last_operation_index]
            # if this_route_B_index == 0:
            #     prepare_time = 30
            # else:
            #     last_B_operation_num = route_B_index[this_route_B_index-1]
            #     last_B_machine_num = selected_machine[product_num][last_B_operation_num]
            #     if last_B_machine_num != last_operation_machine_num:
            #         prepare_time = 30
            # if machine_last_product != product_num:
            #     prepare_time = 30
            last_operation_begin_time = begin_time_o - last_operation_time
            # 更新上道工序
            add_one_item(begin_time_lists[last_operation_machine_num], last_operation_begin_time,
                         machine_operations[last_operation_machine_num])
            add_tree_item(end_time_lists[last_operation_machine_num], job_lists[last_operation_machine_num], operation_lists[last_operation_machine_num],
                          begin_time_o, product_num, last_operation_num,
                          machine_operations[last_operation_machine_num])
            machine_time[last_operation_machine_num] = begin_time_o
            machine_operations[last_operation_machine_num] += 1
            begin_time[last_operation_machine_num][product_num][last_operation_num] = last_operation_begin_time
            end_time[last_operation_machine_num][product_num][last_operation_num] = begin_time_o
            selected_machine[product_num][last_operation_num] = last_operation_machine_num
        if route_type == 3:
            rear_machine_num = 0
            rear_machine_num_time = 0
            candidate_machine_time = machine_time[candidate_machine_list].copy(
            )
            for _ in range(product_joint):
                min_machine_index = np.argmin(candidate_machine_time)
                candidate_machine_time[min_machine_index] += 30000
                selected_machine_num = candidate_machine_list[min_machine_index]
                last_operation_machine_num = selected_machine[product_num][operation_num-1]
                last_operation_end_time = end_time[last_operation_machine_num][product_num][operation_num-1]
                begin_time_o = max(
                    machine_time[selected_machine_num], last_operation_end_time)
                # begin_time_o = begin_time_o + 3-begin_time_o % 3
                operation_time = product_operation_detail[product_num][operation_num][selected_machine_num]
                end_time_o = begin_time_o + operation_time
                add_one_item(begin_time_lists[selected_machine_num],
                             begin_time_o, machine_operations[selected_machine_num])
                add_tree_item(end_time_lists[selected_machine_num], job_lists[selected_machine_num], operation_lists[selected_machine_num],
                              end_time_o, product_num, operation_num,
                              machine_operations[selected_machine_num])
                machine_time[selected_machine_num] = end_time_o
                machine_operations[selected_machine_num] += 1
                begin_time[selected_machine_num][product_num][operation_num] = begin_time_o
                end_time[selected_machine_num][product_num][operation_num] = end_time_o
                if rear_machine_num_time < begin_time_o:
                    rear_machine_num_time = begin_time_o
                    rear_machine_num = selected_machine_num
            selected_machine[product_num][operation_num] = rear_machine_num
    # 生成调度 ### end
    # 初始化最短加工时间
    min_time = 0
    # 计算最短加工时间 ### begin
    for machine_num in range(machine_total_num):
        # 如果机器没有加工过就换下一个机器
        if machine_operations[machine_num] == 0:
            continue
        # 当前机器的最晚完工时间
        min_time_m = end_time_lists[machine_num][machine_operations[machine_num]-1]
        # 更新最短加工时间
        min_time = max(min_time, min_time_m)
    # 计算最短加工时间 ### end
    result[0] = min(result[0], min_time)
    # 返回解


@njit(
    (int64[:, :], int64[:],
     int64[:, :], int64[:]),
    parallel=False, cache=True
)
def update_memory_lib(memory_OS, memory_results,
                      OS, decode_results):
    code_length = OS.shape[1]
    sorted_decode_results = np.argsort(decode_results)
    max_index = np.argmax(memory_results)
    for code_index in sorted_decode_results:
        if decode_results[code_index] < memory_results[max_index]:
            for position in range(code_length):
                memory_OS[max_index][position] = OS[code_index][position]
            memory_results[max_index] = decode_results[code_index]
            max_index = np.argmax(memory_results)
        elif decode_results[code_index] == memory_results[max_index]:
            same_indexs = np.where(
                memory_results == decode_results[code_index])[0]
            exchange_index = max_index
            excheng_H = code_length+1
            for same_index in same_indexs:
                _H = 0
                for position in range(code_length):
                    if memory_OS[same_index][position] == OS[code_index][position]:
                        _H += 1
                if _H == 0:
                    break
                if _H < excheng_H:
                    excheng_H == _H
                    exchange_index = same_index
            for position in range(code_length):
                memory_OS[exchange_index][position] = OS[code_index][position]
            memory_results[exchange_index] = decode_results[code_index]
            max_index = np.argmax(memory_results)
        else:
            break


@njit(
    (int64[:, :],
     int64[:, :],
     int64[:, :],
     int64,
     int64[:], int64[:],
     int64, int64),
    parallel=False, cache=True, fastmath=True)
def tournament_memory(OS,
                      memory_OS,
                      new_OS,
                      tournament_M,
                      decode_results, memory_results,
                      step, max_step):
    '''
    锦标赛选择
    '''
    size = OS.shape[0]
    # 生成新种族 ### begin
    P = np.cos(math.pi*(step)/max_step/2)
    memory_size = memory_OS.shape[0]
    for index in range(size):
        if np.random.random() > P:
            selected_codes = np.random.choice(size, tournament_M)
            selected_codes = np.unique(selected_codes)
            selected_best_code = selected_codes[(np.argmin(
                decode_results[selected_codes]))]
            new_OS[index] = OS[selected_best_code]
        else:
            selected_codes = np.random.choice(size, tournament_M-1)
            selected_codes = np.unique(selected_codes)
            selected_best_code = selected_codes[(np.argmin(
                decode_results[selected_codes]))]
            memory_code = np.random.randint(0, memory_size)
            if decode_results[selected_best_code] > memory_results[memory_code]:
                new_OS[index] = OS[selected_best_code]
            else:
                new_OS[index] = memory_OS[memory_code]
    # 生成新种族 ### end


@njit(
    (int64[:], int64, float64[:]),
    parallel=False
)
def get_crossover_P(decode_results, best_score, crossover_P):
    av_score = decode_results.mean()
    for position in range(decode_results.shape[0]):
        if decode_results[position] >= av_score:
            crossover_P[position] = 1
        elif av_score-best_score == 0:
            crossover_P[position] = 1
        else:
            crossover_P[position] = (av_score-decode_results[position])/(
                av_score-best_score)
    p_mean = crossover_P.mean()
    if p_mean < 0.5:
        crossover_P = crossover_P * (0.5/p_mean)


@njit(
    (int64[:, :], float64[:], int64[:]),
    parallel=False, cache=True)
def POX_crossover(code, Crossover_P,  jobs):
    '''
    基于工件优先顺序的交叉
    '''
    code_size, code_length = code.shape
    jobs_num = jobs.shape[0]
    # 遍历每条基因
    for code_index in range(code_size-1):
        # 判断是否发生交叉
        if np.random.random() >= Crossover_P[code_index]:
            continue
        # 交叉基因 ### begin
        code_1 = code[code_index]
        code_2 = code[code_index+1]
        # 选取工件集
        jobs_set = np.random.choice(jobs_num, np.random.randint(1, jobs_num))
        # 开始交叉
        positions_2 = 0
        for positions_1 in range(code_length):
            if code_1[positions_1] in jobs_set:
                continue
            while(code_2[positions_2] in jobs_set):
                positions_2 += 1
            code_1[positions_1] = code_2[positions_2]
            positions_2 += 1
        # 交叉基因 ### begin
    # 随机排序最后一个


@njit(
    (int64[:, :], float64[:]),
    parallel=False, cache=True)
def random_mutation(code, Mutation_P):
    '''
    随机变异
    '''
    code_size, code_length = code.shape
    for code_index in range(code_size):
        if np.random.random() >= Mutation_P[code_index]:
            continue
        position_1 = np.random.randint(0, code_length)
        position_2 = np.random.randint(0, code_length)
        code[code_index][position_1], code[code_index][position_2] = code[code_index][position_2], code[code_index][position_1]
